package com.nkym.wypan.service.impl;


import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nkym.wypan.constants.CommonConstants;
import com.nkym.wypan.constants.FileConstants;
import com.nkym.wypan.constants.RedisKey;
import com.nkym.wypan.enums.FolderFlag;
import com.nkym.wypan.enums.FileType;
import com.nkym.wypan.enums.PreviewHeader;
import com.nkym.wypan.enums.ResponseStatus;
import com.nkym.wypan.exception.BusinessException;
import com.nkym.wypan.mapper.FileInfoMapper;
import com.nkym.wypan.model.context.fileinfo.*;
import com.nkym.wypan.model.entity.FileChunk;
import com.nkym.wypan.model.entity.FileInfo;
import com.nkym.wypan.model.entity.UserFile;
import com.nkym.wypan.model.entity.UserInfo;
import com.nkym.wypan.model.req.fileinfo.SecUploadReq;
import com.nkym.wypan.model.req.fileinfo.ShardingQueryReq;
import com.nkym.wypan.model.resp.ChunkUploadVo;
import com.nkym.wypan.service.FileChunkService;
import com.nkym.wypan.service.FileInfoService;
import com.nkym.wypan.service.UserFileService;
import com.nkym.wypan.service.UserInfoService;
import com.nkym.wypan.storage.context.*;
import com.nkym.wypan.storage.core.StorageEngine;
import com.nkym.wypan.storage.response.UploadResult;
import com.nkym.wypan.utils.FileUtils;
import com.nkym.wypan.utils.IdUtils;
import com.nkym.wypan.utils.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;


import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 19637
 * @description 针对表【wy_pan_file_info】的数据库操作Service实现
 * @createDate 2023-06-09 09:06:36
 */
@Service
@Slf4j
public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService {
    @Autowired
    private FileInfoMapper fileInfoMapper;
    @Autowired
    private UserFileService userFileService;
    @Autowired
    private UserInfoService userInfoService;
    @Autowired
    Redisson redisson;
    @Autowired
    private StorageEngine storageEngine;
    @Autowired
    private FileChunkService fileChunkService;
    @Autowired
    TransactionTemplate txManger;
    @Autowired
    RedisTemplate<String, Object> redisTemplate;
    private static final String BYTES_STRING = "bytes";

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void secUpload(SecUploadReq secUploadReq) {
        String identifier = secUploadReq.getIdentifier();
        Long parentId = secUploadReq.getParentId();
        Long fileSize = secUploadReq.getFileSize();
        String filename = secUploadReq.getFilename();
        Long uid = UserContextHolder.getId();
        //先判断能否秒传
        FileInfo fileInfo = fileInfoMapper.selectOne(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getIdentifier, identifier));
        if (fileInfo == null) {
            throw new BusinessException(ResponseStatus.SEC_UPLOAD_FAIL);
        }
        //检查用户剩余空间大小 是否能够上传
        boolean canUpload = checkUserSpace(fileSize);
        if (!canUpload) {
            throw new BusinessException(ResponseStatus.USER_NOT_SPACE);
        }
        //映射文件和用户的关系
        UserFile userFile = createUserFileFromFileInfo(fileInfo, uid, parentId, filename);
        userFileService.saveUserFile(userFile);
        //减少用户存储空间
        userInfoService.addUsedSpace(fileSize);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void upload(UploadContext uploadContext) {
        Long userId = UserContextHolder.getId();
        Long parentId = uploadContext.getParentId();
        String filename = uploadContext.getFilename();
        Long fileSize = uploadContext.getFileSize();
        String identifier = uploadContext.getIdentifier();
        InputStream stream = uploadContext.getStream();
        //检查用户剩余空间
        boolean b = checkUserSpace(fileSize);
        if (!b) {
            throw new BusinessException(ResponseStatus.USER_NOT_SPACE);
        }
        //上传文件
        FileUpload upload = new FileUpload();
        upload.setFileSize(fileSize);
        upload.setFilename(filename);


        UploadResult uploadResult = null;
        try {
            upload.setStream(stream);
            uploadResult = storageEngine.upload(upload);
        } catch (IOException e) {
            log.error("{}", e);
            throw new BusinessException(ResponseStatus.UPLOAD_FAIL);
        }


        String realPath = uploadResult.getRealPath();
        String realFilename = realPath.substring(realPath.lastIndexOf("\\") + 1);
        //保存文件记录
        FileInfo fileInfo = assembleFileInfo(realFilename, realPath, fileSize, identifier, userId);

        int insert = fileInfoMapper.insert(fileInfo);
        if (insert <= 0) {
            //删除实体文件
            FileUtil.del(new File(realPath));
            throw new BusinessException("文件信息保存失败");
        }
        //确保插入成功
        FileInfo realFileInfo = fileInfoMapper.selectById(fileInfo.getFileId());
        if (realFileInfo == null) {
            FileUtil.del(new File(realPath));
            throw new BusinessException(ResponseStatus.UPLOAD_FAIL);
        }
        UserFile userFileInfo = createUserFileFromFileInfo(realFileInfo, userId, parentId, filename);
        //保存文件映射关系
        userFileService.saveUserFile(userFileInfo);
        //减少用户存储空间
        userInfoService.addUsedSpace(fileSize);


    }

    @Override
    public void download(String code, HttpServletResponse response) {
        UserFile userFile = (UserFile) redisTemplate.opsForValue().get(RedisKey.SOURCE_CODE_KEY + code);
        if (userFile == null) {
            throw new BusinessException("资源链接失效，请重新获取");
        }
        if (Objects.equals(userFile.getFolderFlag(), FolderFlag.YES.getCode())) {
            throw new BusinessException("文件夹暂不支持下载");
        }
        Long storeFileId = userFile.getRealFileId();
        FileInfo fileInfo = fileInfoMapper.selectById(storeFileId);
        if (fileInfo == null) {
            throw new BusinessException(ResponseStatus.FILE_NOT_EXIST);
        }
        //设置请求头
        addCommonsHeader(response, MediaType.APPLICATION_OCTET_STREAM_VALUE);
        //设置下载需要的请求参数
        addDownloadAttribute(response, userFile, fileInfo);
        //委托存储引擎层下载文件
        Download download = new Download();
        try {
            download.setRealPath(fileInfo.getRealPath());
            download.setOutputStream(response.getOutputStream());
            storageEngine.download(download);
        } catch (Exception e) {
            throw new BusinessException("文件下载失败");
        }
    }

    @Override
    public Set<Integer> shardingQuery(ShardingQueryContext shardingQueryContext) {
        String identifier = shardingQueryContext.getIdentifier();
        Long userId = shardingQueryContext.getUserId();
        String chunkListKey = RedisKey.CHUNK_LIST + userId + ":" + identifier;
        String range = (String) redisTemplate.opsForValue().get(chunkListKey);
        Set<Integer> res = StrUtil.split(range, ",").stream().map(Integer::parseInt).collect(Collectors.toSet());
        if (res.isEmpty()) {
            //断点续传 检查分片是否已经存在
            List<FileChunk> list = fileChunkService.list(new LambdaQueryWrapper<FileChunk>()
                    .select(FileChunk::getChunkNumber)
                    .eq(FileChunk::getCreateUser, userId)
                    .eq(FileChunk::getIdentifier, identifier)
            );
            StringBuilder str = new StringBuilder();
            res = new HashSet<>();
            for (int i = 0; i < list.size(); i++) {
                FileChunk chunk = list.get(i);
                str.append(chunk.getChunkNumber());
                res.add(chunk.getChunkNumber());
                if (i != list.size() - 1) {
                    str.append(",");
                }
            }
            redisTemplate.opsForValue().set(chunkListKey, str.toString());
        }
        //保存到Redis缓存中
        return res;
    }

    @Override
    public String getSourceCode(SourceCodeContext context) {
        Long fileId = context.getFileId();
        Long userId = context.getUserId();
        //校验权限
        UserFile userFile = userFileService.getOne(new LambdaQueryWrapper<UserFile>()
                .eq(UserFile::getFileId, fileId)
                .eq(UserFile::getUserId, userId));
        if (userFile == null) {
            throw new BusinessException(ResponseStatus.ILLEGAL_OPERATION);
        }
        String sourceCode = RandomUtil.randomString(50);
        redisTemplate.opsForValue().set(RedisKey.SOURCE_CODE_KEY + sourceCode, userFile, CommonConstants.THREE, TimeUnit.HOURS);
        return sourceCode;
    }

    @Override
    public ResponseEntity<Resource> preview(String code) {
        UserFile userFile = (UserFile) redisTemplate.opsForValue().get(RedisKey.SOURCE_CODE_KEY + code);
        if (userFile == null) {
            throw new BusinessException("资源连接失效，请重新获取");
        }
        Long storeFileId = userFile.getRealFileId();
        FileInfo fileInfo = fileInfoMapper.selectById(storeFileId);
        if (fileInfo == null) {
            throw new BusinessException(ResponseStatus.FILE_NOT_EXIST);
        }
        File file = new File(fileInfo.getRealPath());
        HttpHeaders headers = new HttpHeaders();
//        headers.setContentDispositionFormData("attachment", userFile.getFilename());
        headers.setContentDisposition(ContentDisposition.inline().build());
        MediaType mimeType = MediaTypeFactory.getMediaType(userFile.getFilename()).orElse(MediaType.APPLICATION_OCTET_STREAM);
        return ResponseEntity
                .ok()
                .headers(headers)
                .contentLength(file.length())
                .contentType(mimeType)
                .body(new FileSystemResource(file))
                ;
    }

    @Override
    public ChunkUploadVo chunkUpload(ChunkUploadContext chunkUploadContext) {
        Long fileSize = chunkUploadContext.getFileSize();
        Long userId = chunkUploadContext.getUserId();
        String identifier = chunkUploadContext.getIdentifier();
        Integer chunkNumber = chunkUploadContext.getChunkNumber();

        // 检查用户可用空间
        if (!checkUserSpace(fileSize)) {
            throw new BusinessException(ResponseStatus.USER_NOT_SPACE);
        }
        String chunkListKey = RedisKey.CHUNK_LIST + userId + ":" + identifier;
        String chunkListStr = (String) redisTemplate.opsForValue().get(chunkListKey);
        Set<Integer> set = StrUtil.split(chunkListStr, ",").stream().map(Integer::parseInt).collect(Collectors.toSet());
        boolean contains = set.contains(chunkNumber);
        if (!contains) {
            //走数据库
            FileChunk chunk = fileChunkService.getOne(new LambdaQueryWrapper<FileChunk>()
                    .eq(FileChunk::getCreateUser, userId)
                    .eq(FileChunk::getIdentifier, identifier)
                    .eq(FileChunk::getChunkNumber, chunkNumber));
            if (chunk != null) {
                if (chunkListStr != null) {
                    chunkListStr = chunkListStr + "," + chunk.getChunkNumber();
                }else{
                    chunkListStr = String.valueOf(chunk.getChunkNumber());
                }
                redisTemplate.opsForValue().set(chunkListKey,chunkListStr);
                contains = true;
            }
        }
        if (!contains) {
            // 使用存储引擎  上传分片
            UploadResult uploadResult = doChunkUpload(chunkUploadContext);
            if (uploadResult == null) {
                throw new BusinessException(ResponseStatus.CHUNK_UPLOAD_FAIL);
            }
            String realPath = uploadResult.getRealPath();
            // 保存分片上传记录
            doSaveFileChunk(chunkUploadContext, realPath);
            if (chunkListStr==null){
                chunkListStr = String.valueOf(chunkNumber);
            }else {
                chunkListStr = chunkListStr + "," + chunkNumber;
            }
            redisTemplate.opsForValue().set(chunkListKey,chunkListStr);
        }
        // 查询分片上传情况
        ChunkUploadVo result = new ChunkUploadVo();
        synchronized (userId.toString().intern()) {
            //缓存
            String key = RedisKey.MERGE_KEY + userId + ":" + identifier;
            Object o = redisTemplate.opsForValue().get(key);
            if (ObjectUtil.isNotNull(o)) {
                result.setCanMerge(FileConstants.CAN_MERGE);
                return result;
            }
            //检查分片上传情况，是否需要合并
            boolean canMerge = checkMerge(chunkUploadContext);
            if (canMerge) {
                redisTemplate.opsForValue().set(key, 1);
                result.setCanMerge(FileConstants.CAN_MERGE);
            }
            return result;
        }
    }

    @Override
    public void chunkMerge(ChunkMergeContext context) {
        Long parentId = context.getParentId();
        String identifier = context.getIdentifier();
        Integer chunkTotalNumber = context.getChunkTotalNumber();
        //用户传过来的文件名
        String filename = context.getFilename();
        Long userId = context.getUserId();
        FileInfo fileInfo;
        //查出所有的分片路径和编号
        List<FileChunk> fileChunkList = fileChunkService.list(new LambdaQueryWrapper<FileChunk>().eq(FileChunk::getIdentifier, identifier).eq(FileChunk::getCreateUser, userId));
        if (fileChunkList.size() != chunkTotalNumber) {
            throw new BusinessException("合并失败，请重新上传");
        }
        //加锁 判断上传同一个文件只需要一个人合并，另外的人秒传即可
        // 如果存在则映射文件关系即可   没必要合并和保存文件信息
        String lockKey = RedisKey.LOCK_PRE + identifier;
        RLock lock = redisson.getLock(lockKey);
        lock.lock(3L, TimeUnit.MINUTES);
        try {
            fileInfo = fileInfoMapper.selectOne(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getIdentifier, identifier));
            //如果identifier唯一文件不存在则保存文件记录 并且合并文件
            txManger.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    try {
                        // 更新信息
                        saveInfo(parentId, identifier, filename, userId, fileInfo, fileChunkList);
                    } catch (Exception e) {
                        // 发生异常时，回滚事务
                        status.setRollbackOnly();
                        throw new BusinessException(e.getMessage());
                    }
                }
            });
            //删除分片记录
            doDeleteChunk(fileChunkList);
            //删除合并请求缓存
            String key = RedisKey.MERGE_KEY + userId + ":" + identifier;
            redisTemplate.delete(key);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void saveInfo(Long parentId, String identifier, String filename, Long userId, FileInfo fileInfo, List<FileChunk> fileChunkList) {
        if (fileInfo == null) {
            //合并分片
            UploadResult result = doMerge(fileChunkList, filename);
            String path = result.getRealPath();
            //系统保存的文件名
            String realFilename = path.substring(path.lastIndexOf("\\") + 1);
            //保存文件
            fileInfo = doSaveFileInfo(result, realFilename, identifier, userId);
        }
        //映射文件关系
        Long fileSize = fileInfo.getFileSize();
        UserFile userFile = createUserFileFromFileInfo(fileInfo, userId, parentId, filename);
        userFileService.saveUserFile(userFile);
        //增加用户已使用空间
        userInfoService.addUsedSpace(fileSize);
    }


    /**
     * ---------------------------------------private--------------------------------------------------
     */
    private FileInfo doSaveFileInfo(UploadResult result, String filename, String identifier, Long userId) {
        String realPath = result.getRealPath();
        Long fileSize = result.getFileSize();
        FileInfo fileInfo = assembleFileInfo(filename, realPath, fileSize, identifier, userId);
        int insert = fileInfoMapper.insert(fileInfo);
        if (insert <= 0) {
            throw new BusinessException(ResponseStatus.CHUNK_MERGE_FAIL);
        }
        //确保记录真实存在
        FileInfo realFileInfo = fileInfoMapper.selectOne(new LambdaQueryWrapper<FileInfo>().eq(FileInfo::getIdentifier, identifier).eq(FileInfo::getCreateUser, userId));
        if (realFileInfo == null) {
            throw new BusinessException(ResponseStatus.CHUNK_MERGE_FAIL);
        }
        return realFileInfo;
    }


    /**
     * 异步删除文件
     *
     * @param fileChunkList
     */
    private void doDeleteChunk(List<FileChunk> fileChunkList) {
        //删除真实物理文件


    }

    private UploadResult doMerge(List<FileChunk> fileChunkList, String filename) {
        List<Merge> mergeList = fileChunkList.stream().map(a -> new Merge(a.getChunkNumber(), a.getRealPath())).collect(Collectors.toList());
        MergeRequest request = new MergeRequest();
        request.setMergeList(mergeList);
        request.setFilename(filename);
        try {
            UploadResult result = storageEngine.mergeChunk(request);
            return result;
        } catch (Exception e) {
            log.error("{}", e);
            throw new BusinessException("合并失败");
        }
    }

    private UploadResult doChunkUpload(ChunkUploadContext chunkUploadContext) {
        Integer chunkNumber = chunkUploadContext.getChunkNumber();
        String identifier = chunkUploadContext.getIdentifier();
        InputStream inputStream = chunkUploadContext.getInputStream();
        Long chunkFileSize = chunkUploadContext.getChunkFileSize();
        //存储引擎上传
        ChunkUpload chunkUpload = new ChunkUpload();
        chunkUpload.setChunkFileSize(chunkFileSize);
        chunkUpload.setIdentifier(identifier);
        chunkUpload.setInputStream(inputStream);
        chunkUpload.setChunkNumber(chunkNumber);
        try {
            UploadResult uploadResult = storageEngine.chunkUpload(chunkUpload);
            return uploadResult;
        } catch (IOException e) {
            log.error("Failed to upload chunk, chunkNumber= {}, identifier={}", chunkNumber, identifier, e);
            throw new BusinessException(ResponseStatus.CHUNK_UPLOAD_FAIL);
        }
    }

    private void doSaveFileChunk(ChunkUploadContext chunkUploadContext, String realPath) {
        FileChunk fileChunk = coverToFileChunk(chunkUploadContext, realPath);
        boolean save = fileChunkService.save(fileChunk);
        if (!save) {
            throw new BusinessException(ResponseStatus.CHUNK_UPLOAD_FAIL);
        }
    }

    private boolean checkMerge(ChunkUploadContext chunkUploadContext) {
        Long userId = chunkUploadContext.getUserId();
        String identifier = chunkUploadContext.getIdentifier();
        Integer totalChunks = chunkUploadContext.getTotalChunks();
        QueryWrapper<FileChunk> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("chunk_number");
        queryWrapper.eq("identifier", identifier);
        queryWrapper.eq("create_user", userId);
        List<FileChunk> list = fileChunkService.list(queryWrapper);
        List<Integer> chunkList = list.stream().map(FileChunk::getChunkNumber).distinct().collect(Collectors.toList());
        if (chunkList.size() == totalChunks) {
            return true;
        }
        return false;
    }

    private FileChunk coverToFileChunk(ChunkUploadContext chunkUploadContext, String realPath) {
        Integer chunkNumber = chunkUploadContext.getChunkNumber();
        String identifier = chunkUploadContext.getIdentifier();
        Long userId = chunkUploadContext.getUserId();
        FileChunk fileChunk = new FileChunk();
        fileChunk.setChunkNumber(chunkNumber);
        fileChunk.setRealPath(realPath);
        fileChunk.setCreateUser(userId);
        fileChunk.setIdentifier(identifier);
        fileChunk.setExpirationTime(DateUtil.offsetDay(new Date(), 3));
        return fileChunk;
    }

    private void addDownloadAttribute(HttpServletResponse response, UserFile userFile, FileInfo fileInfo) {
        try {
            response.addHeader(FileConstants.CONTENT_DISPOSITION_STR, FileConstants.CONTENT_DISPOSITION_VALUE_PREFIX_STR
                    + new String(userFile.getFilename().getBytes(FileConstants.GB2312_STR),
                    FileConstants.IOS_8859_1_STR));
        } catch (UnsupportedEncodingException e) {
            throw new BusinessException("文件下载失败");
        }
        response.setContentLengthLong(fileInfo.getFileSize());
    }

    private void addCommonsHeader(HttpServletResponse response, String value) {
        response.reset();
        response.addHeader("Content-Type", value);
        //添加跨域请求头
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Max-Age", "3600");

    }


    private FileInfo assembleFileInfo(String filename, String realPath, Long fileSize, String identifier, Long userId) {

        String suffix = FileUtils.getSuffix(filename);
        long fileId = IdUtils.getId();
        FileType type = FileUtils.getFileTypeBySuffix(suffix);
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(fileId);
        fileInfo.setFileSize(fileSize);
        PreviewHeader previewHeader = PreviewHeader.fromValue(type.getCode());
        fileInfo.setFilePreviewContentType(previewHeader.contentType);
        fileInfo.setFileSuffix(suffix);
        fileInfo.setFilename(filename);
        fileInfo.setIdentifier(identifier);
        fileInfo.setCreateUser(userId);
        fileInfo.setRealPath(realPath);
        return fileInfo;
    }

    private boolean checkUserSpace(Long fileSize) {
        Long userId = UserContextHolder.getId();
        UserInfo userInfo = userInfoService.getById(userId);
        Long totalSpace = userInfo.getTotalSpace();
        Long useSpace = userInfo.getUseSpace();
        long freeSpace = totalSpace - useSpace;
        return freeSpace > fileSize;
    }

    private UserFile createUserFileFromFileInfo(FileInfo fileInfo, Long uid, Long parentId, String filename) {
        //文件ID
        Long fileId = fileInfo.getFileId();
        UserFile userFile = new UserFile();
        long id = IdUtils.getId();
        // id
        userFile.setFileId(id);
        //对应的真实文件的ID
        userFile.setRealFileId(fileId);
        //用户ID
        userFile.setUserId(uid);
        //所在目录
        userFile.setParentId(parentId);
        //文件名
        userFile.setFilename(filename);

        String suffix = FileUtils.getSuffix(filename).toLowerCase();
        FileType fileType = FileUtils.getFileTypeBySuffix(suffix);
        userFile.setFileType(fileType.getCode());

        userFile.setFileSize(fileInfo.getFileSize());

        userFile.setCreateUser(uid);
        userFile.setDelFlag(FolderFlag.NO.getCode());
        userFile.setUpdateUser(uid);
        userFile.setFolderFlag(FolderFlag.NO.getCode());
        return userFile;
    }

}




